超越基本類型定義。掌握條件類型、模板字面量和字符串操作等高級 TypeScript 功能,構建異常健壯和類型安全的 API。為全球開發者提供的綜合指南。
釋放 TypeScript 的全部潛力:深入研究條件類型、模板字面量和高級字符串操作
在現代軟件開發的世界中,TypeScript 已經遠遠超出了其最初作為 JavaScript 簡單類型檢查器的角色。它已經成為一種複雜的工具,可以被描述為類型級別編程。這種範例允許開發人員編寫在類型本身上運行的代碼,從而創建動態的、自我記錄的且非常安全的 API。這場革命的核心是三個強大的功能協同工作:條件類型、模板字面量類型和一套內置的字符串操作類型。
對於全球希望提升其 TypeScript 技能的開發人員來說,理解這些概念不再是一種奢侈,而是構建可擴展和可維護應用程序的必要條件。本指南將帶您進行深入研究,從基本原則開始,逐步構建到復雜的、真實世界的模式,這些模式展示了它們的組合力量。無論您是構建設計系統、類型安全的 API 客戶端還是複雜的數據處理庫,掌握這些功能都將從根本上改變您編寫 TypeScript 的方式。
基礎:條件類型(`extends` 三元運算符)
從本質上講,條件類型允許您根據類型關係檢查選擇兩種可能的類型之一。如果您熟悉 JavaScript 的三元運算符(condition ? valueIfTrue : valueIfFalse),您會發現語法非常直觀:
type Result = SomeType extends OtherType ? TrueType : FalseType;
在這裡,extends 關鍵字充當我們的條件。它檢查 SomeType 是否可分配給 OtherType。讓我們用一個簡單的例子來分解它。
基本示例:檢查類型
假設我們想要創建一個類型,如果給定的類型 T 是一個字符串,則解析為 true,否則解析為 false。
type IsString
然後我們可以這樣使用這個類型:
type A = IsString<"hello">; // type A is true
type B = IsString<123>; // type B is false
這是基本的構建塊。但是,條件類型的真正力量在與 infer 關鍵字結合使用時才能釋放。
`infer` 的力量:從內部提取類型
infer 關鍵字是一個遊戲規則改變者。它允許您在 extends 子句中聲明一個新的泛型類型變量,有效地捕獲您正在檢查的類型的一部分。將其視為類型級別的變量聲明,它從模式匹配中獲取其值。
一個經典的例子是解包包含在 Promise 中的類型。
type UnwrapPromise
讓我們分析一下:
T extends Promise:這會檢查T是否為Promise。如果是,TypeScript 會嘗試匹配結構。infer U:如果匹配成功,TypeScript 會捕獲Promise解析為的類型,並將其放入一個名為U的新類型變量中。? U : T:如果條件為真(T是一個Promise),則結果類型為U(解包的類型)。否則,結果類型只是原始類型T。
用法:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
這種模式非常常見,以至於 TypeScript 包括內置的實用程序類型,例如 ReturnType,它使用相同的原則來提取函數的返回類型。
可分配條件類型:使用聯合
條件類型一個引人入勝且至關重要的行為是,當被檢查的類型是“裸”泛型類型參數時,它們會變成可分配的。這意味著如果您將聯合類型傳遞給它,條件將單獨應用於聯合的每個成員,並且結果將被收集回新的聯合中。
考慮一種將類型轉換為該類型數組的類型:
type ToArray
如果我們將聯合類型傳遞給 ToArray:
type StrOrNumArray = ToArray
結果不是 (string | number)[]。因為 T 是一個裸類型參數,所以條件是分佈式的:
ToArray變成string[]ToArray變成number[]
最終結果是這些單獨結果的聯合:string[] | number[]。
這種可分配屬性對於過濾聯合非常有用。例如,內置的 Extract 實用程序類型使用它從可分配給 U 的聯合 T 中選擇成員。
如果您需要阻止這種可分配行為,您可以將類型參數包裝在 extends 子句兩側的元組中:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
有了這個堅實的基礎,讓我們探索如何構造動態字符串類型。
在類型級別構建動態字符串:模板字面量類型
在 TypeScript 4.1 中引入的模板字面量類型允許您定義形狀像 JavaScript 模板字面量字符串的類型。它們使您可以從現有字符串字面量類型中連接、組合和生成新的字符串字面量類型。
語法與您期望的完全一樣:
type World = "World";
type Greeting = `Hello, ${World}!`; // type Greeting is "Hello, World!"
這可能看起來很簡單,但它的力量在於將其與聯合和泛型結合使用。
聯合和排列
當模板字面量類型涉及聯合時,它會擴展為包含每個可能的字符串排列的新聯合。這是一種生成一組定義良好的常量的強大方法。
想像一下定義一組 CSS 邊距屬性:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
MarginProperty 的結果類型是:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
這非常適合創建類型安全的組件屬性或函數參數,其中只允許特定的字符串格式。
與泛型結合
模板字面量在與泛型一起使用時真正大放異彩。您可以創建工廠類型,根據某些輸入生成新的字符串字面量類型。
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
這種模式是創建動態、類型安全的 API 的關鍵。但是,如果我們需要修改字符串的大小寫,例如將 "user" 更改為 "User" 以獲取 "onUserChange" 怎麼辦?這就是字符串操作類型發揮作用的地方。
工具包:內置字符串操作類型
為了使模板字面量更加強大,TypeScript 提供了一組用於操作字符串字面量的內置類型。這些就像實用程序函數,但用於類型系統。
大小寫修飾符:`Uppercase`、`Lowercase`、`Capitalize`、`Uncapitalize`
這四種類型的功能與它們的名稱所暗示的完全相同:
Uppercase:將整個字符串類型轉換為大寫。type LOUD = Uppercase<"hello">; // "HELLO"Lowercase:將整個字符串類型轉換為小寫。type quiet = Lowercase<"WORLD">; // "world"Capitalize:將字符串類型的第一個字符轉換為大寫。type Proper = Capitalize<"john">; // "John"Uncapitalize:將字符串類型的第一個字符轉換為小寫。type variable = Uncapitalize<"PersonName">; // "personName"
讓我們重新訪問之前的示例,並使用 Capitalize 改進它,以生成傳統的事件處理程序名稱:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
現在我們擁有了所有的部分。讓我們看看它們如何結合起來解決復雜的、真實世界的問題。
綜合:將所有三者結合起來以實現高級模式
這是理論與實踐相結合的地方。通過將條件類型、模板字面量和字符串操作編織在一起,我們可以構建非常複雜和安全的類型定義。
模式 1:完全類型安全的事件發射器
目標:創建一個通用的 EventEmitter 類,其方法(如 on()、off() 和 emit())是完全類型安全的。這意味著:
- 傳遞給方法的事件名稱必須是有效的事件。
- 傳遞給
emit()的有效負載必須與為該事件定義的類型匹配。 - 傳遞給
on()的回調函數必須接受該事件的正確有效負載類型。
首先,我們定義一個從事件名稱到其有效負載類型的映射:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
現在,我們可以構建通用的 EventEmitter 類。我們將使用一個泛型參數 Events,它必須擴展我們的 EventMap 結構。
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// `on` 方法使用泛型 `K`,它是我們 Events 映射的鍵
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// `emit` 方法確保有效負載與事件的類型匹配
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
讓我們實例化並使用它:
const appEvents = new TypedEventEmitter
// 這是類型安全的。有效負載被正確推斷為 { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript 會在此處引發錯誤,因為 "user:updated" 不是 EventMap 中的鍵
// appEvents.on("user:updated", () => {}); // Error!
// TypeScript 會在此處引發錯誤,因為有效負載缺少 'name' 屬性
// appEvents.emit("user:created", { userId: 123 }); // Error!
這種模式為傳統上是許多應用程序中非常動態且容易出錯的部分提供了編譯時安全性。
模式 2:嵌套對象的類型安全路徑訪問
目標:創建一個實用程序類型 PathValue,它可以使用點表示法字符串路徑 P(例如,"user.address.city")確定嵌套對象 T 中值的類型。
這是一種高度先進的模式,展示了遞歸條件類型。
這是實現,我們將對其進行分解:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
讓我們用一個例子來追踪它的邏輯:PathValue
- 初始調用:
P是"a.b.c"。這與模板字面量`${infer Key}.${infer Rest}`匹配。 Key被推斷為"a"。Rest被推斷為"b.c"。- 第一次遞歸:該類型檢查
"a"是否為MyObject的鍵。如果是,它會遞歸調用PathValue。 - 第二次遞歸:現在,
P是"b.c"。它再次與模板字面量匹配。 Key被推斷為"b"。Rest被推斷為"c"。- 該類型檢查
"b"是否為MyObject["a"]的鍵,並遞歸調用PathValue。 - 基本情況:最後,
P是"c"。這不匹配`${infer Key}.${infer Rest}`。類型邏輯下降到第二個條件:P extends keyof T ? T[P] : never。 - 該類型檢查
"c"是否為MyObject["a"]["b"]的鍵。如果是,則結果為MyObject["a"]["b"]["c"]。如果不是,則為never。
與輔助函數一起使用:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
這種強大的類型可以防止路徑中出現拼寫錯誤導致的運行時錯誤,並為深度嵌套的數據結構提供完美的類型推斷,這是處理複雜 API 響應的全球應用程序中常見的挑戰。
最佳實踐和性能注意事項
與任何強大的工具一樣,明智地使用這些功能非常重要。
- 優先考慮可讀性:複雜的類型會很快變得難以讀懂。將它們分解為更小的、命名良好的輔助類型。使用註釋來解釋邏輯,就像對待複雜的運行時代碼一樣。
- 了解 `never` 類型:
never類型是您處理錯誤狀態和在條件類型中過濾聯合的主要工具。它表示不應發生的狀態。 - 注意遞歸限制:TypeScript 對類型實例化具有遞歸深度限制。如果您的類型過於深度嵌套或無限遞歸,編譯器將引發錯誤。確保您的遞歸類型具有清晰的基本情況。
- 監控 IDE 性能:極其複雜的類型有時會影響 TypeScript 語言服務器的性能,導致編輯器中的自動完成和類型檢查速度變慢。如果您遇到速度減慢的情況,請查看是否可以簡化或分解複雜的類型。
- 知道何時停止:這些功能用於解決類型安全和開發人員體驗的複雜問題。不要使用它們來過度設計簡單的類型。目標是提高清晰度和安全性,而不是增加不必要的複雜性。
結論
條件類型、模板字面量和字符串操作類型不僅僅是孤立的功能;它們是一個緊密集成的系統,用於在類型級別執行複雜的邏輯。它們使我們能夠超越簡單的註釋,構建能夠深刻了解自身結構和約束的系統。
通過掌握這三個,您可以:
- 創建自記錄的 API:類型本身成為文檔,引導開發人員正確使用它們。
- 消除整個類別的錯誤:類型錯誤在編譯時被捕獲,而不是由生產中的用戶捕獲。
- 改善開發人員體驗:享受豐富的自動完成功能和內聯錯誤消息,即使對於代碼庫中最動態的部分也是如此。
擁抱這些高級功能將 TypeScript 從安全網轉變為開發中強大的合作夥伴。它允許您將複雜的業務邏輯和不變量直接編碼到類型系統中,從而確保您的應用程序對於全球受眾而言更加健壯、可維護和可擴展。